package erfgame.frontend.desktop.view.terrain.iso;

import java.awt.Dimension;
import java.awt.Point;
import java.awt.Rectangle;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.swing.SwingUtilities;

import erfgame.core.Pair;
import erfgame.core.Point3D;
import erfgame.core.Point3DAndRotation;
import erfgame.core.Rectangle3D;
import erfgame.core.Rotation;
import erfgame.core.dao.ObjectDAO;
import erfgame.core.entity.Entity;
import erfgame.core.entity.Physical;
import erfgame.core.graph.simple.Node;
import erfgame.core.graphics.renderer.RendererContext;
import erfgame.core.graphics.renderer.chain.EntityRendererChainFactory;
import erfgame.core.graphics.renderer.chain.RendererChain;
import erfgame.core.world.World;
import erfgame.core.world.terrain.BlockEntityPositions;
import erfgame.core.world.terrain.BlockSurfaceImages;
import erfgame.core.world.terrain.PixelDepth;
import erfgame.core.world.terrain.TerrainUtils;
import erfgame.core.world.terrain.insertion.SurfaceInsertion;
import erfgame.core.world.terrain.rotated.RotatedWorld;

public class ModdingBlockSurfaceImageViewport extends Rectangle {
	private static final Logger log = Logger.getLogger( ModdingBlockSurfaceImageViewport.class.getName() );
	
	BlockSurfaceImages[][][] surfaceImages;
	
	private Rotation currentRotation;
	
	private int blockX;
	private int blockY;
	private int worldX;
	private int worldY;
	private RotatedWorld world;
	private ObjectDAO<Point3DAndRotation, BlockSurfaceImages> bsiDAO;
	private ObjectDAO<Point3D, BlockEntityPositions> entityPositionsDAO;
	private int blockBuffer;
	private int currentLayer;
	
	private ThreadPoolExecutor executor;
	private SurfaceLoader[][][] loaders;
	
	private Dimension bounds;
	
	private ViewportListener listener;
	private RendererContext rendererContext;
	private EntityRendererChainFactory rendererFactory;
	
	public ModdingBlockSurfaceImageViewport( 
			BlockSurfaceImages[][][] surfaceImages, 
			int surfaceImagesWidth, 
			int surfaceImagesHeight, 
			RotatedWorld world,
			ObjectDAO<Point3DAndRotation, BlockSurfaceImages> bsiDAO, 
			ObjectDAO<Point3D, BlockEntityPositions> entityPositionsDAO,
			int blockBuffer, 
			Rotation rotation,
			Dimension bounds, 
			RendererContext rendererContext, 
			EntityRendererChainFactory rendererFactory 
	) {
		this.surfaceImages = surfaceImages;
		this.width = surfaceImagesWidth;
		this.height = surfaceImagesHeight;
		this.world = world;
		this.bsiDAO = bsiDAO;
		this.entityPositionsDAO = entityPositionsDAO;
		this.blockBuffer = blockBuffer;
		this.x = -surfaceImagesWidth;
		this.y = -surfaceImagesHeight * 2;
		this.currentRotation = rotation;
		//this.executor = Executors.newSingleThreadExecutor();
		this.executor = (ThreadPoolExecutor)Executors.newFixedThreadPool(1);
		this.loaders = new SurfaceLoader[ surfaceImagesWidth ][ surfaceImagesHeight ][ world.getBlocksZ() ];
		this.bounds = bounds;
		this.rendererContext = rendererContext;
		this.rendererFactory = rendererFactory;
	}
	
	public Rotation getCurrentRotation() {
		return this.currentRotation;
	}
	
	public int getCurrentLayer() {
		return currentLayer;
	}

	public void setCurrentLayer(int currentLayer) {
		this.currentLayer = currentLayer;
	}

	public void setViewportListener( ViewportListener listener ) {
		this.listener = listener;
	}
	
	public void startLoading( Point3DAndRotation p ) {
		startLoading( 
				p.x % this.width, 
				p.y % this.height, 
				p.z, 
				p 
		);
	}
	
	public Point getBlockPosition( int vx, int vy, Point into ) 
	{
		Point aroundTimes2 = world.getOriginalWorld().getWorldBlockMidPointTimes2();
		Point toLoad = currentRotation.rotateInBox(
				vx, vy, 
				aroundTimes2.x, 
				aroundTimes2.y, 
				into
		);
		return toLoad;
	}

	public Point getViewPosition( int bx, int by, Point into ) 
	{
		// TODO there has got to be a better way than this, seems to be 
		// countering a bug somewhere else
		Point aroundTimes2 = world.getOriginalWorld().getWorldBlockMidPointTimes2();
		switch( currentRotation.ordinal())
		{
		default:
		case Rotation.ORDINAL_0:
			into.x = bx;
			into.y = by;
			break;
		case Rotation.ORDINAL_90:
			into.x = by;
			into.y = world.getOriginalWorld().getBlocksX() - bx - 1;
			break;
		case Rotation.ORDINAL_180:
			currentRotation.rotateInBox(
					bx, by, 
					aroundTimes2.x, 
					aroundTimes2.y, 
					into
			);
			break;
		case Rotation.ORDINAL_270:
			into.y = bx;
			into.x = world.getOriginalWorld().getBlocksY() - by - 1;
			break;
		}
		return into;
	}

	public void startLoading( int bx, int by, int bz, Point3DAndRotation p ) {
		// NOTE: this method is unsynchronized, you should synchronize externally as appropriate
//		log.info( "loading ("+p+") into ("+bx+","+by+","+bz+")" );
		// TODO this test shouldn't be required
		//if( world.getBlocksX() > p.x && p.x >= 0 && world.getBlocksY() > p.y && p.y >= 0 ) {
			SurfaceLoader oldLoader = loaders[bx][by][bz];
			if( oldLoader != null ) {
				if( !oldLoader.worldPoint.equals( p ) ) {
					// this loader is no longer valid
					boolean result = this.executor.remove( oldLoader );
					if( !result ) {
						log.log( Level.INFO, "could not stop unneccessary loading of "+oldLoader.worldPoint );
					}
				} else {
					// it's already loading
					log.log( Level.INFO, "already loading "+oldLoader.worldPoint );
					return;
				}
			}
			Point3D b = new Point3D( bx, by, bz );
			SurfaceLoader loader;
			loader = new SurfaceLoader( b, p, null, null );
			surfaceImages[bx][by][bz] = null;
			loaders[bx][by][bz] = loader;
			executor.execute( loader );
//		}
	}
	
	public BlockSurfaceImages[] getBlockSurfaceImages( int bx, int by ) {
		BlockSurfaceImages[] result;
		if( contains(bx, by) ) {
			int x = bx % this.width;
			int y = by % this.height;
			result = this.surfaceImages[x][y];
		} else {
			result = null;
		}
		return result;
	}
	
	public void setWorldOffsets( int worldX, int worldY ) {
//		int maxx = world.getBlocksX() << world.getBlockWidthShift();
//		int maxy = world.getBlocksY() << world.getBlockHeightShift();
//		Point worldPosition = world.getRotation().rotateInBox(originalWorldX, originalWorldY, maxx, maxy, new Point());
//		int worldX = worldPosition.x;
//		int worldY = worldPosition.y;
		
		int currentLayer = this.currentLayer;
		int oldBlockX = this.x;
		int oldBlockY = this.y;
//		this.worldX = Math.max( 0, Math.min( worldX, world.getWorldWidth() - this.bounds.width ) );
//		this.worldY = Math.max( 0, Math.min( worldY, world.getWorldHeight() - (this.bounds.height - this.world.getBlockDepth()) * 2 ) );
		this.worldX = Math.max( 0, Math.min( worldX, world.getWorldWidth() - this.bounds.width ) );
		this.worldY = Math.min( Math.max( 0, worldY ), world.getWorldHeight() - (this.bounds.height - this.world.getBlockDepth()) * 2 );
		this.blockX = this.worldX / this.world.getBlockWidth();
		this.blockY = getRearWorldY() / this.world.getBlockHeight(); 
		int newBlockX = Math.max( 0, Math.min( this.blockX - this.blockBuffer, this.world.getBlocksX() - width ) );
		int newBlockY = Math.max( 0, Math.min( this.blockY - this.blockBuffer, this.world.getBlocksY() - height ) );
		
		// start loading the diff
		int dx = newBlockX - oldBlockX;
		int dy = newBlockY - oldBlockY;
		
		if( dx != 0 || dy != 0 ) {
			log.log(Level.INFO, "viewing ("+newBlockX+","+newBlockY+")");
			
			Point aroundTimes2 = world.getOriginalWorld().getWorldBlockMidPointTimes2();
			
			Rectangle r = new Rectangle(oldBlockX, oldBlockY, width, height); 
			
			this.x = newBlockX;
			this.y = newBlockY;
			int expectedSize = Math.abs(dx)*height + Math.abs(dy)*width;
			ArrayList<Pair<Point, Point>> pointsToLoad;
			pointsToLoad = new ArrayList<Pair<Point, Point>>( expectedSize );

			// calculate the blocks that have changed
			log.log( Level.INFO, "--------- "+aroundTimes2+"/"+getCurrentRotation() );
			for( int x=this.width; x>0; ) {
				x--;
				int tx = newBlockX + x;
				for( int y=this.height; y>0; ) {
					y--;
					int ty = newBlockY + y;
					//Point toLoad = currentRotation.rotate(tx, ty, aroundTimes2, new Point());
					Point toLoad = currentRotation.rotateInBox(
							tx, ty, 
							aroundTimes2.x, 
							aroundTimes2.y, 
							new Point()
					);
					World originalWorld = world.getOriginalWorld();
					
					boolean valid = toLoad.x>=0 && toLoad.x<originalWorld.getBlocksX() && toLoad.y>=0 && toLoad.y<originalWorld.getBlocksY() && !r.contains(tx, ty);
					if( valid ) {
						Point toFill = new Point( tx, ty );
						//log.log( Level.INFO, ""+toFill+" -> "+toLoad+" valid="+valid );
						pointsToLoad.add( new Pair<Point, Point>( toFill, toLoad ) );
					}
				}
			}
			// start loading the buffer
			for( int z=0; z<world.getBlocksZ(); z++ ) {
				int tz = (currentLayer + z) % world.getBlocksZ();
				for( int i=pointsToLoad.size(); i>0; ) {
					i--;
					Pair<Point, Point> p = pointsToLoad.get( i );
					Point toFill = p.getFirst();
					Point toLoad = p.getSecond();
					startLoading( 
							toFill.x % width,
							toFill.y % height, 
							tz,
							new Point3DAndRotation( 
									toLoad.x, 
									toLoad.y, 
									tz, 
									currentRotation
							)
					);
				}
			}
		}
	}

	public int getSurfaceImagesWidth() {
		return width;
	}

	public int getSurfaceImagesHeight() {
		return height;
	}

	public int getBlockX() {
		return blockX;
	}

	public int getBlockY() {
		return blockY;
	}
	
	public int getWorldX() {
		return this.worldX;
	}
	
	public int getWorldY() {
		return this.worldY;
	}
	
	public int getRearWorldY() {
		return this.worldY - (this.world.getWorldDepth()-this.world.getBlockDepth())*2;
	}
	
	public Rectangle getBounds() {
		return new Rectangle(
				getWorldX(), 
				getRearWorldY(), 
				getSurfaceImagesWidth() << world.getBlockWidthShift(), 
				getSurfaceImagesHeight() << world.getBlockHeightShift()
		);
	}
	
	private class SurfaceLoader implements Runnable {
		
		private Point3DAndRotation worldPoint;
		private Point3D blockPoint;
		private PixelDepth[][] intoDepths;
		private int[][] intoSurfaces;
		
		public SurfaceLoader( Point3D blockPoint, Point3DAndRotation worldPoint, PixelDepth[][] intoDepths, int[][] intoSurfaces ) {
			this.blockPoint = blockPoint;
			this.worldPoint = worldPoint;
			this.intoDepths = intoDepths;
			this.intoSurfaces = intoSurfaces;
		}

		public void run() {
			Runnable r = new Runnable() {
				public void run() {

			Point3D key = new Point3D(worldPoint);
			
			final BlockSurfaceImages bsi = bsiDAO.retrieveObject(
					worldPoint
			);
			
			bsi.clearTemporaryInsertions();
			BlockEntityPositions entityPositions = entityPositionsDAO.retrieveObject(key);
			List<Entity> entities = entityPositions.getEntities();
			
			ArrayList<SurfaceInsertion> entityInsertions = new ArrayList<SurfaceInsertion>(entities.size());
			for( int i=entities.size(); i>0; ) {
				i--;
				Entity entity = entities.get( i );
				// see if the entity is actually rendered as part of this BSI from this angle or if it just overlaps 
				
				try
				{
					RendererChain renderer = rendererFactory.getRendererChain(world.getOriginalWorld(), entity, getCurrentRotation()); 
					Physical physical = (Physical)entity;
					Point3D position = physical.getPosition();
					Point grabPoint = renderer.getHead().getGrabPoint();
					Dimension dimension = renderer.getHead().getDimension();
	
					//TODO rotate the grab point
					int startX = position.x - grabPoint.x;
					int startY = position.y;
					int startZ = position.z - grabPoint.y;			
					
					Rectangle3D bounds = new Rectangle3D(
							startX, 
							startY, 
							startZ, 
							startX + dimension.width, 
							startY, 
							startZ + dimension.height
					); 
					bounds.rotate(world.getRotation(), new Point( position.x*2, position.y*2 ));
	
					// get the blocks that it overlaps
					int startBlockX = bounds.minX >> world.getBlockWidthShift();
					int endBlockX = bounds.maxX >> world.getBlockWidthShift();
					int startBlockY = bounds.minY >> world.getBlockHeightShift();
					int endBlockY = bounds.maxY >> world.getBlockHeightShift();
					// z should always match up
					int startBlockZ = Math.max( 0, bounds.minZ / world.getBlockDepth() );
					int endBlockZ = Math.min(world.getBlocksZ()-1, bounds.maxZ / world.getBlockDepth());
					if( startBlockX <= key.x && endBlockX >= key.x && startBlockY <= key.y && endBlockY >= key.y ) {
						((IsometricTerrainView)rendererContext).knowEntity(entity);
						
						SurfaceInsertion insertion = TerrainUtils.createInsertion(
								world, 
								key, 
								renderer 
						);
						if( insertion != null ) {
							entityInsertions.add( insertion );
						}
					}
				} catch( Exception ex ) {
					log.log(Level.WARNING, "error loading renderer for "+entity+" skipping renderer", ex );
				}
			}
			bsi.addInsertions(entityInsertions);

			List<Node<SurfaceInsertion>> insertions = bsi.getSurfaceInsertions();
			for( int i=insertions.size(); i>0; ) {
				i--;
				SurfaceInsertion insertion = insertions.get( i ).getValue();
				RendererChain rendererChain = insertion.getRendererChain();
				if( !rendererChain.isAttachedTo(rendererContext) ) {
					rendererChain.attach( rendererContext );
				}
			}
			BlockSurfaceImages oldBsi = surfaceImages[blockPoint.x][blockPoint.y][blockPoint.z];
			if( oldBsi != null ) {
				// TODO : should probably thread in AWT
				List<Node<SurfaceInsertion>> oldInsertions = oldBsi.getSurfaceInsertions();
				for( int i=oldInsertions.size(); i>0; ) {
					i--;
					SurfaceInsertion insertion = oldInsertions.get( i ).getValue();
					insertion.getRendererChain().detach( rendererContext );
				}
			}
			//synchronized( ModdingBlockSurfaceImageViewport.this ) 
			{
				// TODO use unrotated position instead of doing this
							Point viewPosition = getViewPosition( worldPoint.x, worldPoint.y, new Point() );
							loaders[blockPoint.x][blockPoint.y][blockPoint.z] = null;
							if( contains(viewPosition) ) {
								surfaceImages[blockPoint.x][blockPoint.y][blockPoint.z] = bsi;
								listener.blockLoaded(worldPoint.x, worldPoint.y, worldPoint.z);
							} else {
								log.warning( "outdated loading of "+worldPoint+ " not in "+super.toString() );
							}
						}
					}
//				
//				if( viewBlockX <= worldPoint.x && 
//					viewBlockX + surfaceImagesWidth > worldPoint.x &&
//					viewBlockY <= worldPoint.y &&
//					viewBlockY + surfaceImagesHeight > worldPoint.y ) {
//					loaders[blockPoint.x][blockPoint.y][blockPoint.z] = null;
//					surfaceImages[blockPoint.x][blockPoint.y][blockPoint.z] = bsi;
//					listener.blockLoaded(worldPoint.x, worldPoint.y, worldPoint.z);
//				} else {
//					log.warning( "outdated loading of "+worldPoint );
//					loaders[blockPoint.x][blockPoint.y][blockPoint.z] = null;
//				}
			};
			SwingUtilities.invokeLater(r);
		}
	};

}

